# -*- coding: utf-8 -*-
import os, urllib, magic
from time import time

from Settings import Settings
from Exceptions.LibraryException import LibraryException
from MediaCollection.Database import Database
from MediaCollection.Audio.Artist import Artist
from MediaCollection.Audio.Cd import Cd
from MediaCollection.Audio.Genre import Genre
from MediaCollection.Audio.Track import Track
from MediaCollection.Util.TagReader.Reader import Reader
from MediaCollection.Util.WebFetching.LastFM import LastFMFetcher

"""
Der File-Reader ist zuständig für das Einlesen von Dateien und dem 
anschließenden Einfügen der (Meta-)Daten in die Datenbank. Hierbei wird der 
angegebene Ordner rekursiv nach Audio-Dateien durchsucht. Befindet sich in einem
Ordner eine Bilddatei, so wird diese für das entsprechende Album genutzt. Die 
vorgeschlagene Ordnerstruktur für die Audio-Dateien ist: 
    [Artist]/[Album]/[0-x] - [Track].[flac/ogg/mp3]
Sollte kein Bild im Ordner vorhanden sein, so kann der FileReader über den 
Konstruktor (Parameter lastFM=True) angewiesen werden, Bilder für die 
entsprechenden Artists und Alben über lastFM zu holen. Diese werden in dem in 
den Settings eingestellten Ordner gespeichert.

@author: Philipp Dermitzel
@date: 2011-04.05
@version: 1.9.3
"""
class FileReader(object):

    """
    ----------------------------------------------------------------------------
    Klassenvariablen
    ----------------------------------------------------------------------------
    """
    
    """
    Die Klassen-Instanz der programmweiten Settings.
    @type: Settings
    """
    settings = Settings()
    
    """
    ----------------------------------------------------------------------------
    Konstruktor
    ----------------------------------------------------------------------------
    """
    
    """
    Der Konstruktor.
    @param path: Der Pfad des zu durchsuchenden Ordners.
    @type path: String
    @param lastFM: Über diesen Parameter kann das Fetching von LastFM-Bildern 
        aktiviert werden.    
    @type lastFM: boolean
    @param action: Wird dieser Parameter gesetzt, kann entweder der Ordner ge-
        updatet oder die Datensätze der im angegebenen Ordner vorhandenen
        Dateien gelöscht werden.
        ACHTUNG: Die Daten werden REKURSIV gelöscht! Die Dateien selbst werden 
        dabei nicht geändert/gelöscht!
    @type update: String [update/remove]
    """
    def __init__(self, path, lastFM = False, action = None):
        """
        Der LastFM-Fetcher.
        @type: LastFMFetcher
        """
        self.__lastFM = LastFMFetcher()
        
        """
        Über die URLib wird das Bild aus dem Internet geladen.
        @type: unknown
        """
        self.__image = urllib.URLopener()
        
        """
        Über python-magic wird das Bildformat der heruntergeladenen Bilder 
        bestimmt.
        @type unknown
        """
        self.__ms = magic.open(magic.MAGIC_NONE)
        
        """
        Speichert, ob Bilder von lastFM geladen werden sollen
        @type boolean
        """
        self.__fetchFromLastFM = lastFM
        
        """
        Der Timestamp der letzten Änderung an der MedieCollection.
        @type: int
        """
        db = Database()
        try:
            db.select('settingValue', 'Settings', {'settingKey':'lastUpdate'})
            self.__lastUpdate = int(db.dbCursor.fetchone()[0])
        except: self.__lastUpdate = 0
        
        if action != "clean":
            if action == "update": self.__updateFolder(os.path.abspath(path))
            elif action == "remove": self.__removeFolder(os.path.abspath(path))
            else: self.__readFolder(os.path.abspath(path))
            self.__insertTimestampIntoDb()
        self.__cleanMediaCollection()
        
    """
    ----------------------------------------------------------------------------
    Methoden der Klasse
    ----------------------------------------------------------------------------
    """
    
    """
    Die Hauptmethode. Wird vom Konstruktor und rekursiv aufgerufen.
    @param folderPath: Der Pfad zum aktuellen Ordner.    
    @type folderPath: String
    """
    def __readFolder(self, folderPath):
        for dirItem in os.listdir(folderPath):
            curPath = folderPath + "/" + dirItem
            if os.path.isdir(curPath): self.__readFolder(curPath)
            else: self.__readFile(curPath)
            
    """
    Updatet einen Ordner. Es wird geprüft, ob im/am Ordner Änderungen 
    vorgenommen wurden. Ist dies der Fall, so wird rekursiv jeder Unterordner
    geprüft und jede im aktuellen Pfad vorhandene Datei neu eingelesen.
    Hierbei ist zu beachten, dass das Speichern im Format wie in der Klassen-
    Beschreibung angegeben, zu einem erheblichen Geschwindigkeitsvorteil führt!
    Bei größeren Datei-Sammlungen in wenigen Ordnern, können Änderungen hohe
    Laufzeiten erzeugen!
    @param folderPath: Der Pfad zum Ordner.
    @type folderPath: String
    """    
    def __updateFolder(self, folderPath):   
        if (self.__lastUpdate < int(os.stat(folderPath).st_mtime)
        or self.__lastUpdate < int(os.stat(folderPath).st_ctime)):
            for dirItem in os.listdir(folderPath):
                curPath = folderPath + "/" + dirItem
                if os.path.isdir(curPath): self.__updateFolder(curPath)
                else: self.__readFile(curPath, update=True)
        else: pass
    
    """
    Löscht die Datensätze der im Ordner vorhandenen Dateien. ACHTUNG: Die Daten
    werden REKURSIV gelöscht! Die Dateien selbst werden nicht gelöscht!
    @param folderPath: Der Pfad zum Ordner.
    @type folderPath: String
    """
    def __removeFolder(self, folderPath):
        for dirItem in os.listdir(folderPath):
            curPath = folderPath + "/" + dirItem
            if os.path.isdir(curPath): self.__removeFolder(curPath)
            else: 
                try:
                    curTrack = Track(path=curPath)
                    db = Database()
                    db.execute("DELETE FROM Track WHERE id = %d" 
                        %(curTrack.getId()))
                except: pass
    
    """
    Die Methode zum Einlesen einer (Audio-)Datei. Es werden die Meta-Daten 
    eingelesen und, falls möglich, sowohl der Artist, das Album und der Track in
    der Datenbank gespeichert. Falls eingestellt, werden außerdem Bilder von 
    LastFM geholt.
    @param filePath: Der Pfad zur aktuellen Pfad.
    @type filePath: String
    @param update: Wenn dieser Parameter gesetzt wird, wird die aktuelle Datei
        geupdatet. Hierbei wird der Track (nicht Artist und Album!) aus der
        Datenbank gelöscht und neu eingefügt.
    @type update: boolean
    """    
    def __readFile(self, filePath, update = False):
        if update:
            try:
                oldTrack = Track(path=filePath)
                db = Database()
                db.execute("DELETE FROM Track WHERE id = %d" 
                    %(oldTrack.getId()))
            except: pass
        audio = Reader(filePath)
        tagData = audio.getTagData()
        if tagData is not False:
            artist = None;
            cd = None;
            try: artist = Artist(name=tagData['artist'])
            except Exception:
                artist = Artist()
                artist.setValue('name', tagData['artist'])
                if tagData['artistMbId'] is not None: 
                    artist.setValue('mbId', tagData['artistMbId'])
                else: artist.setValue('mbId', None)
                if tagData['sort'] is not None: 
                    artist.setValue('sort', tagData['sort'])
                if (self.__fetchFromLastFM and tagData['artist'] is not None):
                    try: 
                        imageURL = self.__lastFM.getArtistImage(
                            tagData['artist'])
                        imagePath = "%s/%s" %(os.path.abspath(
                            FileReader.settings.getSetting('imageDir')), 
                            tagData['artist'])
                        self.__image.retrieve(imageURL, imagePath)
                        realImagePath = "%s.%s" %(imagePath, 
                            self.__detectFileType(imagePath))
                        os.rename(imagePath, realImagePath)
                        artist.setValue('imgPath', realImagePath)
                    except: pass
                artist.insertIntoDb()
            try: cd = Cd(artist=artist.getId(), name=tagData['cd'])
            except Exception:
                if self.__fetchFromLastFM: 
                    cdArt = self.__readImage(os.path.dirname(filePath), 
                        tagData['cd'], tagData['artist'])
                else: cdArt = self.__readImage(os.path.dirname(filePath))
                cd = Cd()
                cd.setValue('artist', int(artist.getId()))
                cd.setValue('name', tagData['cd'])
                if tagData['year'] is not None: 
                    cd.setValue('year', tagData['year'])
                else: cd.setValue('year', None)
                if tagData['cdMbId'] is not None: 
                    cd.setValue('mbId', tagData['cdMbId'])
                else: cd.setValue('mbId', None)
                if tagData['genres'] is not None: 
                    for genre in tagData['genres']: 
                        try: genreH = Genre(name=genre)
                        except:
                            genreH = Genre()
                            genreH.setValue('name', genre).insertIntoDb()
                        cd.addGenre(int(genreH.getId()))                    
                if cdArt is not None: cd.setValue('imgPath', cdArt) 
                else: cd.setValue('imgPath', None)
                cd.insertIntoDb()
            try: 
                track = Track(name=tagData['title'], artist=int(artist.getId()),
                    cd=int(cd.getId()))
            except Exception:
                track = Track()
                track.setValue('length', tagData['length'])
                track.setValue('path', filePath)
                if tagData['title'] is not None: 
                    track.setValue('name', tagData['title'])
                else: track.setValue('name', None)
                if artist.getId() is not None: 
                    track.setValue('artist', int(artist.getId()))
                else: track.setValue('artist', None)
                if cd.getId() is not None: 
                    track.setValue('cd', int(cd.getId()))
                else: track.setValue('cd', None)
                if tagData['trackOnCd'] is not None: 
                    track.setValue('trackOnCd', int(tagData['trackOnCd']))
                else: track.setValue('trackOnCd', None)
                track.insertIntoDb()
        else: pass
    
    """
    Liest ein Bild ein. Falls eingestellt und kein Bild im Ordner der Datei 
    vorhanden, wird ein Bild von lastFM geholt.
    @param folderPath: Der Pfad zur (Audio-)Datei.
    @type folderPath: String
    @param albumName: Der Name des Album (nur für das lastFM-Fetching benötigt).
    @type albumName: String
    @param artistName: Der Name des Artists (nur für das lastFM-Fetching 
        benötigt).
    @type artistName: String
    @return: Der Pfad zum Bild oder None
    @return: String/None
    """
    def __readImage(self, folderPath, albumName = None, artistName = None):
        for dirItem in os.listdir(folderPath):
            parts = dirItem.split('.')
            fileException = parts[len(parts) - 1].lower()
            if fileException in FileReader.settings.getSetting('imageFormats'): 
                return "%s/%s" %(os.path.abspath(folderPath), dirItem)
        if (self.__fetchFromLastFM 
        and albumName is not None 
        and artistName is not None):
            try: 
                imageURL = self.__lastFM.getAlbumImage(albumName, artistName)
                imagePath = "%s/%s - %s" %(os.path.abspath(FileReader.settings.
                    getSetting('imageDir')), artistName, albumName)
                self.__image.retrieve(imageURL, imagePath)
                realImagePath = "%s.%s" %(imagePath, 
                    self.__detectFileType(imagePath))
                os.rename(imagePath, realImagePath)
                return realImagePath
            except: return None
        else: return None
    
    """
    Liefert das Bildformat eines Bildes, soweit in den Settings unterstützt 
    (siehe Settings)
    @param filePath: Der Pfad zum Bild.
    @type filePath: String
    @return: Die Dateiendung
    @rtype: String
    """    
    def __detectFileType(self, filePath):
        self.__ms.load()
        fileExtension = ""
        for ft in FileReader.settings.getSetting('imageFormats'): 
            t = str(self.__ms.file(filePath).lower())
            if t.find(ft) > -1: 
                fileExtension = ft
                break
        return fileExtension
    
    """
    Speichert den Timestamp des letzten Updates der MediaCollection in der
    Datenbank.
    """
    def __insertTimestampIntoDb(self):
        db = Database()
        db.update('Settings', {'settingValue':int(time())}, 
            {'settingKey':'lastUpdate'})
    
    """
    Räumt die Datenbank auf. Hierbei werden folgende Datensätze gelöscht:
    - Alle CDs, die keine Track besitzen
    - Alle Artists, die weder CDs noch Tracks besitzen
    - Alle Genrezuweisungen, die zu nicht existierenden CDs gehören
    - Alle Genres, die keiner CD zugeordnet sind
    """        
    def __cleanMediaCollection(self):
        db = Database()
        db.savepoint("cleanUp")
        try:
            db.execute("""
                DELETE FROM Cd 
                WHERE id NOT IN (SELECT cd FROM Track)
            """)
            db.execute("""
                DELETE FROM Artist
                WHERE id NOT IN (SELECT artist FROM Cd)
                AND id NOT IN (SELECT artist FROM Track)
            """)
            db.execute("""
                DELETE FROM hasGenre 
                WHERE cd NOT IN (SELECT id FROM Cd)
            """)
            db.execute("""
                DELETE FROM Genre
                WHERE id NOT IN (SELECT genre FROM hasGenre)
            """)
        except: 
            db.rollback("cleanUp")
            raise LibraryException("Die Library konnte nicht aufgeräumt werden")
